CDKの開発を爆速に!手元の変更を自動デプロイするCDK Watchを試してみた
こんにちは。MAD事業部のきんじょーです。
TypeScriptやGoなどコンパイルが必要な言語でLambdaを開発をしていると、AWSマネジメントコンソールでコードの変更ができず、ちょっとした変更を試すにも、ローカルでコードを修正しデプロイする必要があります。
それを通常のcdk deploy
でデプロイすると、CloudFormationを挟むためスタックの大きさによっては数十分かかり、ついついお布団に入りたくなります。
その問題を解消するために、CDKにはCloudFormationを挟まずAWS SDKを使用して直接リソースを更新するhotswap deploymentsという機能があります。
つい先日、ファイルの変更を検知して継続的にhotswapを実行するcdk watch
というコマンドがあることを知ったので、早速試してみました。
CDKのhotswapを使用したことがない方は以下を参照してみてください。
CDK Watchでできること
忙しい方へ向けて先にまとめです。
cdk watch
を使用するとプロジェクト内のファイルの変更を検知して自動でデプロイが開始される- デフォルトでは
--hotswap
で実行されるが、--hotswap
に対応していない変更は通常のCloudFormationでデプロイされる。(明示的に--no-hotswap
や--no-rollback
を指定することも可能) - 監視対象のファイルは
cdk.json
で指定する - MFAを有効化している環境では、変更が検知される度にトークンを求められるのであまり意味がない
ExpressをFargateで動かすアプリケーションで検証
記事冒頭のブログに習い、TypeScriptとExpressでコンテナ化されたWebアプリケーションをデプロイして検証します。
環境
macOS Big Sur 11.5.1 Apple M1 node v14.17.6 cdk 2.3.0
cdk watch
自体はCDK v1でも使用可能ですが、以下のサンプルコードはv2の形式でCDKのモジュールをインポートしています。
Step1. プロジェクトの作成
TypeScriptでCDKプロジェクトを立ち上げます。
$ mkdir cdk-watch $ cd cdk-watch $ npx cdk init --language=typescript
以下のようにプロジェクトが作成されました。
ROOT ├── README.md ├── bin │ └── cdk-watch.ts ├── cdk.json ├── jest.config.js ├── lib │ └── cdk-watch-stack.ts ├── node_modules ├── package-lock.json ├── package.json ├── test │ └── cdk-watch.test.ts └── tsconfig.json
Step2. アプリケーションコードの追加
アプリケーションコードを配置するディレクトリを作成します。
$ mkdir docker-app
{ "name": "simple-webpage", "version": "1.0.0", "description": "Demo web app running on Amazon ECS", "license": "MIT-0", "dependencies": { "express": "^4.17.1" }, "devDependencies": { "@types/express": "^4.17.13" } }
package.json
を作成しアプリケーションの依存ライブラリにExpressを追加します
<!DOCTYPE html> <html lang="en" dir="ltr"> <head> <meta charset="utf-8"> <title>Simple Webpage </title> </head> <body> <div align="center" <h2>Hello World</h2> <hr width="25%"> </div> </body> </html>
サンプルのアプリケーションでホストするHTMLファイル追加します。
import * as express from 'express'; const app = express(); app.get("/", (req, res) => { res.sendFile(__dirname + "/index.html"); }); app.listen(80, function () { console.log("server started on port 80"); });
Webサーバーを立ち上げるExpressのコードを追加します。
FROM --platform=amd64 node:alpine RUN mkdir -p /usr/src/www WORKDIR /usr/src/www COPY . . RUN npm install --production-only CMD ["node", "webpage.js"]
最後にアプリケーションを起動するDockerfileを追加します。
M1 Macを使用しているため、Fargateでイメージを動かすのに明示的にplatformの指定が必要で少しハマりました。
Step3. インフラコードの追加
import { Stack, StackProps, aws_ec2 as ec2, aws_ecs as ecs, aws_ecs_patterns as ecs_patterns, } from 'aws-cdk-lib'; import { Construct } from 'constructs'; export class CdkWatchStack extends Stack { constructor(scope: Construct, id: string, props?: StackProps) { super(scope, id, props); const vpc = new ec2.Vpc(this, 'Vpc', { maxAzs: 2, natGateways: 1, }); new ecs_patterns.ApplicationLoadBalancedFargateService(this, 'EcsService', { vpc, taskImageOptions: { image: ecs.ContainerImage.fromAsset('docker-app'), containerPort: 80, }, }); } }
Expressを起動するFargateのコンテナを、lib配下のcdk-watch-stack.ts
を更新して定義します。
{ "app": "npx ts-node --prefer-ts-exts bin/cdk-watch.ts", "context": { "@aws-cdk/aws-apigateway:usagePlanKeyOrderInsensitiveId": true, "@aws-cdk/core:enableStackNameDuplicates": true, "aws-cdk:enableDiffNoFail": true, "@aws-cdk/core:stackRelativeExports": true, "@aws-cdk/aws-ecr-assets:dockerIgnoreSupport": true, "@aws-cdk/aws-secretsmanager:parseOwnedSecretName": true, "@aws-cdk/aws-kms:defaultKeyPolicies": true, "@aws-cdk/aws-s3:grantWriteWithoutAcl": true, "@aws-cdk/aws-ecs-patterns:removeDefaultDesiredCount": true, "@aws-cdk/aws-rds:lowercaseDbIdentifier": true, "@aws-cdk/aws-efs:defaultEncryptionAtRest": true, "@aws-cdk/aws-lambda:recognizeVersionProps": true, "@aws-cdk/aws-cloudfront:defaultSecurityPolicyTLSv1.2_2021": true }, "build": "cd docker-app && tsc" }
cdk.json
に"build"をキーに指定したコマンドは、cdkをデプロイする前に自動的に実行されますが、cdk watch
によるデプロイの場合も同様です。
今回はデプロイ前にTypeScriptをJavaScriptにトランスパイルする必要があるため、tscコマンドを追加します。
ここまでの変更で、アプリケーションは以下のような構成になりました。
ROOT ├── README.md ├── bin │ ├── cdk-watch.d.ts │ ├── cdk-watch.js │ └── cdk-watch.ts ├── cdk.json ├── docker-app │ ├── Dockerfile │ ├── index.html │ ├── node_modules │ ├── package.json │ ├── webpage.ts ├── jest.config.js ├── lib │ └── cdk-watch-stack.ts ├── node_modules ├── package-lock.json ├── package.json ├── test │ └── cdk-watch.test.ts └── tsconfig.json
Step4. 手動でデプロイ
必要とするライブラリをインストールし、手動でデプロイしてみます。
今回、CDK v2をはじめてデプロイするのでcdk bootstrapが必要でした。
$ yarn install $ npx cdk bootstrap $ npx cdk deploy ✅ CdkWatchStack Outputs: CdkWatchStack.EcsServiceLoadBalancerDNS6D595ACE = CdkWa-EcsSe-1ER2RZFKM2OQ-1332853417.ap-northeast-1.elb.amazonaws.com CdkWatchStack.EcsServiceServiceURLE56F060F = http://CdkWa-EcsSe-1ER2RZFKM2OQ-1332853417.ap-northeast-1.elb.amazonaws.com Stack ARN: arn:aws:cloudformation:ap-northeast-1:999999999999:stack/CdkWatchStack/46fd2a60-6966-11ec-b8a9-0a8a9c9afbe7
無事デプロイに成功しました!
Outputsに出力されたサービスのURLを開くと、index.htmlが表示されました。
Step4. CDK Watchを試してみる
cdk watch
を実行すると、ファイルの変更検知が開始されました。
$ npx cdk watch 'watch' is observing directory '' for changes 'watch' is observing directory 'bin' for changes 'watch' is observing directory 'docker-app' for changes 'watch' is observing directory 'lib' for changes 'watch' is observing the file 'bin/cdk-watch.ts' for changes 'watch' is observing the file 'docker-app/Dockerfile' for changes 'watch' is observing the file 'docker-app/index.html' for changes 'watch' is observing the file 'docker-app/package.json' for changes 'watch' is observing the file 'docker-app/webpage.ts' for changes 'watch' is observing the file 'docker-app/yarn.lock' for changes 'watch' is observing the file 'lib/cdk-watch-stack.ts' for changes
<!DOCTYPE html> <html lang="en" dir="ltr"> <head> <meta charset="utf-8"> <title>Simple Webpage </title> </head> <body> <div align="center" <h2>Hello World v2</h2> <hr width="25%"> </div> </body> </html>
ここで、HTMLファイルを一部変更してみます。
cdk watch
を走らせていたターミナルで変更が検知され、デプロイが開始されました!
Detected change to 'docker-app/index.html' (type: change). Triggering 'cdk deploy' ⚠️ The --hotswap flag deliberately introduces CloudFormation drift to speed up deployments ⚠️ It should only be used for development - never use it for your production Stacks!
デプロイ完了後、さきほどのURLを開くとコンテナイメージが更新されていることを確認できます。
デプロイの実行中にファイルを更新しても、進行中のデプロイが完了するまではキューに入らないようで、排他制御も効いていました。
Detected change to 'docker-app/index.html' (type: change) while 'cdk deploy' is still running. Will queue for another deployment after this one finishes
監視対象のファイル
{ "app": "npx ts-node --prefer-ts-exts bin/cdk-watch.ts", "watch":{ "include":"src/main/**"、 "exclude":"target/*" } }
cdk.jsonにinclude
とexclude
で指定をします。
デフォルトのinclude
は"**/*"
で、プロジェクトに含まれるすべてのファイルを対象とし、exclude
は.
始まりの隠しファイルやCDKの出力先、node_modules
を除外しています。
cdk watchで指定できるオプション
--hotswap
デフォルトでは--hotswap
が有効になっており、CloudFormationを介さずに爆速でデプロイが走ります。
現時点で、--hotswap
を使用してデプロイできる変更は以下の通りです。
- Lambda Functionsのコードとタグの変更
- Lambdaのバージョンとエイリアスの変更
- StepFunctionsステートマシンの定義の変更
- ECSサービスのコンテナアセットの変更
- S3に静的ホスティングした資材の変更
- CodeBuildプロジェクトのソースと環境の変更
上記以外の変更は通常のCloudFormationで、自動的にデプロイが走ります。
また、--no-hotswap
を明示的に指定することもできます。
--no-rollback
--no-rollback
を指定することで、デプロイ失敗時にStackがロールバックされるのを防ぐことができます。
MFAとの相性
MFAを使用している場合、cdk deploy
を走らせる度にトークンの入力が必要です。
cdk watch
の場合も、変更を検知する度にMFAトークンを求められるため、watchモードの恩恵はあまり感じられませんでした。
Detected change to 'docker-app/index.html' (type: change). Triggering 'cdk deploy' MFA token for arn:aws:iam::999999999999:mfa/role-name:
まとめ
cdk deploy --hotswap
を使うようになってからLambdaをデプロイする待ち時間が減り、開発効率が大幅に上がっていましたが、cdk watch
はデプロイコマンドを打つ手間さえ不要なので、MFAさえ無ければどんどん使って行こうと思いました。
皆さんもデプロイの手間を減らしてCDKで爆速開発をしていきましょう!
以上、MAD事業部のきんじょーでした。